﻿using Daemon.Common.Cache;
using Daemon.Data.Infrastructure.Auth;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Daemon.Common.Auth
{
	/// <summary>
	/// client auth cache.
	/// </summary>
	public class ClientAuthCache
	{
		private const string AUTHCACHE_CATEGORYNAME = "AuthInfo";
		private const int AUTHCACHE_EXPIRATION_HOUR = 24;
		private const int RETRY_NUMBER = 3;
		private const int RETRY_INTERVAL_MILLISECOND = 500;

		private readonly IDaemonDistributedCache _daemonDistributedCache;

		private IHttpContextAccessor _httpContextAccessor;

		/// <summary>
		/// Initializes a new instance of the <see cref="ClientAuthCache"/> class.
		/// constractor.
		/// </summary>
		public ClientAuthCache(IDaemonDistributedCache daemonDistributedCache, IHttpContextAccessor httpContextAccessor)
		{
			_daemonDistributedCache = daemonDistributedCache;
			_httpContextAccessor = httpContextAccessor;
		}

		/// <summary>
		/// add client user entity. async version
		/// </summary>
		/// <param name="username">user name.</param>
		/// <param name="authInfo">auth info.</param>
		public void AddClientUserEntryAsync(string username, IAuthInfo authInfo)
		{
			authInfo.UserEntity = authInfo.UserEntity.ToSimple();
			var cacheItem = new CacheWrapper<IAuthInfo>
			{
				RedisExpiredTime = DateTime.Now.AddHours(1),
				CacheData = authInfo,
			};
			bool SetRedisCacheAsync()
			{
				_daemonDistributedCache.HashSet<CacheWrapper<IAuthInfo>>(GetAuthCacheKey(), username.ToLowerInvariant(), cacheItem);
				return true;
			}

			Retry(SetRedisCacheAsync, nameof(AddClientUserEntryAsync), $"Fail to add cache, key = {username}, endpoint = {GetEndpoint()}.");
		}

		/// <summary>
		/// clear client user entity.
		/// </summary>
		/// <param name="clientKey">clientkey.</param>
		/// <param name="username">user name.</param>
		public void ClearClientUserEntry(string clientKey, string username)
		{
			bool DeleteCache()
			{
				_daemonDistributedCache.HashDelete<CacheWrapper<IAuthInfo>>(GetAuthCacheKey(), new string[] { username.ToLowerInvariant() });
				return true;
			}

			Retry(DeleteCache, nameof(ClearClientUserEntry), clientKey, $"Fail to clear cache, key = {username}, endpoint = {GetEndpoint()}.");
		}

		/// <summary>
		/// clear client user entity. async version
		/// </summary>
		/// <param name="clientKey">clientkey.</param>
		/// <param name="username">user name.</param>
		public void ClearClientUserEntryAsync(string username)
		{
			bool DeleteCache()
			{
				return _daemonDistributedCache.HashDelete<IAuthInfo>(GetAuthCacheKey(), new string[] { username.ToLowerInvariant() }) > 0;
			}

			Retry(DeleteCache, nameof(ClearClientUserEntryAsync), $"Fail to clear cache, key = {username}, endpoint = {GetEndpoint()}.");
		}

		/// <summary>
		/// clear client user entries.
		/// </summary>
		/// <param name="clientKey"></param>
		/// <param name="usernames"></param>
		public void ClearClientUserEntries(string clientKey, IEnumerable<string> usernames)
		{
			if (usernames == null || !usernames.Any())
			{
				return;
			}

			var userList = usernames.Select(username => username.ToLowerInvariant()).ToList();
			bool DeleteCache()
			{
				_daemonDistributedCache.HashDelete<CacheWrapper<IAuthInfo>>(GetAuthCacheKey(), userList);
				return true;
			}

			Retry(DeleteCache, nameof(ClearClientUserEntries), clientKey, $"Fail to clear caches, keys = {string.Join(",", usernames)}, endpoint = {GetEndpoint()}.");
		}

		/// <summary>
		/// get auth info by client user.
		/// </summary>
		/// <param name="username">user name.</param>
		/// <returns>auth info, hasRedisError.</returns>
		public IAuthInfo GetAuthInfoByClientUser(string username)
		{
			AuthInfoCache GetCache()
			{
				return _daemonDistributedCache.HashGet<AuthInfoCache>(GetAuthCacheKey(), username?.ToLowerInvariant());
			}

			var cache = Retry(GetCache, nameof(GetAuthInfoByClientUser), $"Fail to get cache, key = {username}, endpoint = {GetEndpoint()}.");

			if (cache != null && (DateTime.Now - cache.RedisExpiredTime) <= TimeSpan.FromHours(AUTHCACHE_EXPIRATION_HOUR))
			{
				return cache.CacheData;
			}

			return null;
		}

		public Dictionary<string, CacheWrapper<AuthInfoEF>> GetAuthInfosByClientUsers(string clientkey, List<string> usernames)
		{
			CacheWrapper<AuthInfoEF> GetCache(string username)
			{
				return _daemonDistributedCache.HashGet<CacheWrapper<AuthInfoEF>>(GetAuthCacheKey(), username.ToLowerInvariant());
			}

			Dictionary<string, CacheWrapper<AuthInfoEF>> authInfos = new Dictionary<string, CacheWrapper<AuthInfoEF>>();

			foreach (string username in usernames)
			{
				CacheWrapper<AuthInfoEF> cache = Retry(() => GetCache(username), nameof(GetAuthInfosByClientUsers), clientkey, $"Fail to get cache, key = {username}, endpoint = {GetEndpoint()}.");

				if (cache != null && (DateTime.Now - cache.RedisExpiredTime) <= TimeSpan.FromHours(AUTHCACHE_EXPIRATION_HOUR))
				{
					authInfos.Add(username, cache);
				}
			}

			return authInfos;
		}

		public void UpdateAuthInfoCache(string clientkey, string username, CacheWrapper<AuthInfoEF> authInfoCache)
		{
			bool SetRedisCache()
			{
				_daemonDistributedCache.HashSet(GetAuthCacheKey(), username.ToLowerInvariant(), authInfoCache);
				return true;
			}

			Retry(SetRedisCache, nameof(UpdateAuthInfoCache), clientkey, $"Fail to update cache, key = {clientkey}, endpoint = {GetEndpoint()}.");
		}

		private string GetAuthCacheKey()
		{
			return $"{AUTHCACHE_CATEGORYNAME}";
		}

		private T Retry<T>(Func<T> action, string functionName, string errorMsg)
		{
			var tries = 0;
			try
			{
				while (tries < RETRY_NUMBER)
				{
					try
					{
						return action();
					}
					catch (Exception ex)
					{
						tries++;
						if (tries == RETRY_NUMBER)
						{
							throw ex;
						}
					}

					Task.Delay(RETRY_INTERVAL_MILLISECOND);
				}
			}
			catch (Exception ex)
			{

			}

			return default;
		}

		private T Retry<T>(Func<T> func, string functionName, string clientKey, string errorMsg)
		{
			var tries = 0;
			try
			{
				while (tries < RETRY_NUMBER)
				{
					try
					{
						return func();
					}
					catch (Exception ex)
					{
						tries++;
						if (tries == RETRY_NUMBER)
						{
							throw ex;
						}

					}

					Thread.Sleep(RETRY_INTERVAL_MILLISECOND);
				}
			}
			catch (Exception ex)
			{
			}

			return default;
		}

		private string GetEndpoint()
		{
			return _httpContextAccessor.HttpContext.Request.Method + ": " + _httpContextAccessor.HttpContext.GetEndpoint().DisplayName;
		}
	}

	public sealed class AuthInfoCache
	{
		public AuthInfoEF CacheData { get; set; } = null;

		public System.DateTime RedisExpiredTime { get; set; } = System.DateTime.UtcNow;
	}


	public class CacheWrapper<T>
	{
		public DateTime RedisExpiredTime { get; set; }

		public T CacheData { get; set; }
	}
}
